package com.tasks.positions;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import com.tasks.positions.domain.Direction;
import com.tasks.positions.domain.Operation;
import com.tasks.positions.domain.Position;
import com.tasks.positions.domain.PositionId;
import com.tasks.positions.domain.TradeEvent;

/**
 * The instance of this class manages multiple positions and trade events.
 * 
 * Note that instance of this object is not thread-safe. To guarantee
 * predictable results, ensure that you hold the instance's monitor before
 * invoking any of the methods of a instance that might be operated on in more
 * than one thread.
 */
public class PositionManager {
	private final static Logger LOGGER = Logger.getLogger(PositionManager.class
			.getName());

	/**
	 * Keep track of all positions based on their ids.
	 */
	private final Map<PositionId, Position> positionsMap;

	/**
	 * Keep track of all trades with the highest version among all positions.
	 * 
	 * Trade id is a key and the trade with the highest version is the value.
	 */
	private final Map<Integer, TradeEvent> recentTradesMap;

	/**
	 * The position rule determines how to calculate the trade's quantity based
	 * on the trade event's properties.
	 */
	private final PositioningRule positioningRule;

	public PositionManager() {
		this.positionsMap = new HashMap<PositionId, Position>();

		this.recentTradesMap = new HashMap<Integer, TradeEvent>();

		this.positioningRule = new PositionRuleImpl();
	}

	/**
	 * Adds new trade event to the position determined by trade's account and
	 * trade's security id.
	 * 
	 * @param trade
	 *            the incoming trade event.
	 */
	public void addTrade(TradeEvent trade) {
		LOGGER.info("Adding new trade event: " + trade);

		updateRecentTrades(trade);

		final PositionId positionId = new PositionId(trade.getAccountNumber(),
				trade.getSecurityId());

		Position existingPosition = positionsMap.get(positionId);

		if (existingPosition == null) {
			existingPosition = new Position(positionId);
			positionsMap.put(positionId, existingPosition);
		}

		existingPosition.addTrade(trade);
	}

	private void updateRecentTrades(TradeEvent trade) {
		if (recentTradesMap.containsKey(trade.getId())) {
			TradeEvent existingTrade = recentTradesMap.get(trade.getId());

			if (existingTrade.getVersion() < trade.getVersion()) {
				LOGGER.info("Updating the existing trade event with the new version: " + trade);
				
				recentTradesMap.put(trade.getId(), trade);
			} else {
				LOGGER.info("Ignoring the trade event since the newer version exists: " + trade);
			}
		} else {
			LOGGER.info("Saving the trade event: " + trade);
			
			recentTradesMap.put(trade.getId(), trade);
		}
	}

	public long getQuantity(PositionId positionId) {
		LOGGER.info("Calling getQuantity");

		Position position = positionsMap.get(positionId);

		if (position == null) {
			final String msg = "Position with the id: " + positionId
					+ " doesn't exist";
			LOGGER.severe(msg);
			throw new InvalidPositionException(msg);
		}

		List<TradeEvent> trades = position.getAllTrades();

		long quantity = 0;

		for (TradeEvent trade : trades) {
			TradeEvent existingTrade = recentTradesMap.get(trade.getId());

			// skip current trade from calculations if we have more recent
			// version in another position
			if (existingTrade.getVersion() > trade.getVersion()) {
				continue;
			}

			quantity += positioningRule.getQuantity(trade);
		}

		LOGGER.info("The calculated quantity for the position: " + positionId
				+ " is: " + quantity);

		return quantity;
	}

	public List<TradeEvent> getTrades(PositionId positionId) {
		Position position = positionsMap.get(positionId);

		if (position == null) {
			final String msg = "Position with the id: " + positionId
					+ " doesn't exists";
			LOGGER.severe(msg);
			throw new InvalidPositionException(msg);
		}

		return position.getAllTrades();
	}

	private final class PositionRuleImpl implements PositioningRule {
		@Override
		public int getQuantity(TradeEvent trade) {
			int result = 0;

			final int multiplier;
			if (Direction.SELL == trade.getDirection()) {
				multiplier = -1;
			} else {
				multiplier = 1;
			}
			
			LOGGER.info("The trade multiplier value: " + multiplier);

			if (Operation.NEW == trade.getOperation()
					|| Operation.AMEND == trade.getOperation()) {
				return multiplier * trade.getQuantity();
			} else {
				// return 0 for cancel transactions
				return result;
			}
		}
	}

}
